﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Universe.MySqlTests.Dapper.Contrib
{
    using System.Collections.Concurrent;
    using System.Data;
    using System.Diagnostics;
    using System.Reflection;

    using global::Dapper;
    using global::Dapper.Contrib.Extensions;

    using Microsoft.CSharp.RuntimeBinder;

    static partial class MyDapperEx
    {
        private static int MaxSize = 0;
        private static object MaxSizeSync = new object();

        static readonly object[] NoIndex = new object[0];

        public static void BatchUpsertByKeys<T>(this IDbConnection con, IEnumerable<T> list, int? commandTimeout = null,
            IDbTransaction transaction = null)
        {
            // if exists(select 1 from 
        }
        
        public static void BatchInsert<T>(this IDbConnection con, IEnumerable<T> list, int? commandTimeout = null, IDbTransaction transaction = null)
        {
            const int MAX_PARAMETERS = 1000;
            var type = typeof(T);
            var allProperties = GetAllPropertiesByType(type);
            if (!allProperties.Any())
                return;

            var tableName = type.Name;
            int pos = 1;
            var fields = string.Join(",", allProperties.Select(x => GetColumnName(x)));

            String sqlPrefix = "Insert " + tableName + " (" + fields + ") Values ";

            int portionSize = Math.Max(MAX_PARAMETERS / allProperties.Count(), 1);
            foreach (var portion in Partitions(list, portionSize))
            {
                int parPos = 0;
                List<string> allItemsValues = new List<string>();
                DynamicParameters pars = new DynamicParameters();
                for (int i = 0; i < portion.Count; i++)
                {
                    var item = portion[i];
                    StringBuilder itemVals = new StringBuilder();
                    foreach (var pi in allProperties)
                    {
                        var value = pi.GetValue(item, NoIndex);
                        var parName = "@P" + parPos;
                        pars.Add(parName, value);
                        itemVals.Append(itemVals.Length == 0 ? "" : ",").Append(parName);
                        parPos++;
                    }

                    allItemsValues.Add(itemVals.ToString());
                    pos++;
                }

                var sql = sqlPrefix + string.Join(", ", allItemsValues.Select(x => "(" + x + ")")) + ";";
                int maxSize;
                lock (MaxSizeSync)
                {
                    MaxSize = Math.Max(MaxSize, sql.Length);
                    maxSize = MaxSize;
                }

                // Trace.WriteLine("SQL" + Environment.NewLine + sql + Environment.NewLine);
                Stopwatch at = Stopwatch.StartNew();
                con.Execute(sql, pars, commandTimeout: commandTimeout, transaction: transaction);
                // Trace.WriteLine("Insert " + type.Name + "*" + portion.Count + " using " + sql.Length.ToString("n0") + " bytes command: " + at.Elapsed + "; Max Command Length=" + maxSize.ToString("n0"));
            }

        }

        static IEnumerable<List<T>> Partitions<T>(IEnumerable<T> list, int pageSize)
        {
            List<T> buffer = new List<T>(pageSize);
            foreach (var i in list)
            {
                buffer.Add(i);
                if (buffer.Count >= pageSize)
                {
                    yield return buffer;
                    buffer.Clear();
                }
            }

            if (buffer.Count > 0)
                yield return buffer;
        }

        static ConcurrentDictionary<PropertyInfo, string> _NamesOfProperties = new ConcurrentDictionary<PropertyInfo, string>();
        private static string GetColumnName(PropertyInfo propertyInfo)
        {
            string ret;
            if (_NamesOfProperties.TryGetValue(propertyInfo, out ret))
                return ret;

            ret = Encapsulate(propertyInfo.Name);

            var columnattr = propertyInfo.GetCustomAttributes(true).SingleOrDefault(attr => attr.GetType().Name == "ColumnAttribute") as dynamic;
            if (columnattr != null)
            {
                ret = Encapsulate(columnattr.Name);
                Trace.WriteLine(String.Format("Column overrided: {0} of {1} :: {2}", ret, propertyInfo.DeclaringType, propertyInfo.Name));
            }

            _NamesOfProperties[propertyInfo] = ret;
            return ret;
        }

        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> _TypeProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>>();
        private static IEnumerable<PropertyInfo> GetAllPropertiesByType(Type type)
        {
            IEnumerable<PropertyInfo> pis;
            if (_TypeProperties.TryGetValue(type.TypeHandle, out pis))
            {
                return pis;
            }

            var properties = type.GetProperties().Where(IsEditable).ToArray();
            _TypeProperties[type.TypeHandle] = properties;
            return properties;
        }

        private static bool IsEditable(PropertyInfo pi)
        {
            var attributes = pi.GetCustomAttributes(false);
            if (attributes.Length > 0)
            {
                dynamic write = attributes.FirstOrDefault(x => x.GetType().Name == "EditableAttribute");
                if (write != null)
                {
                    return write.AllowEdit;
                }
            }

            return true;
        }

        private static bool IsReadOnly(PropertyInfo pi)
        {
            var attributes = pi.GetCustomAttributes(false);
            if (attributes.Length > 0)
            {
                dynamic write = attributes.FirstOrDefault(x => x.GetType().Name == "ReadOnlyAttribute");
                if (write != null)
                {
                    return write.IsReadOnly;
                }
            }
            return false;
        }

        private static string Encapsulate(string databaseword)
        {
            return databaseword;
        }

        static ConcurrentDictionary<Type, TypeInfo> _TypeInfo = new ConcurrentDictionary<Type, TypeInfo>();

        static TypeInfo GetTypeInfo(Type type)
        {
            TypeInfo ret;
            if (_TypeInfo.TryGetValue(type, out ret))
                return ret;

            ret = new TypeInfo() {TableName = GetTableName(type)};
            List<PropertyInfo> properties = type.GetProperties().ToList();
            ret.All = properties.ConvertAll(x => new PropInfo() {Property = x, ColumnName = GetColumnName(x), Name = x.Name});
            ret.Keys = KeyProperties(properties).ConvertAll(x => new PropInfo() { Property = x, ColumnName = GetColumnName(x), Name = x.Name });

            _TypeInfo[type] = ret;
            return ret;
        }

        // Either [Key] attr or Id name
        private static List<PropertyInfo> KeyProperties(List<PropertyInfo> properties)
        {

            var allProperties = properties;
            var keyProperties = allProperties.Where(p => p.GetCustomAttributes(true).Any(attr => attr.GetType().Name == "KeyAttribute")).ToList();

            if (keyProperties.Count == 0)
            {
                var idProp = allProperties.Where(p => p.Name.ToLower() == "id").FirstOrDefault();
                if (idProp != null)
                {
                    keyProperties.Add(idProp);
                }
            }

            return keyProperties;
        }


        private static string GetTableName(Type type)
        {
            //var tableName = String.Format("[{0}]", type.Name);
            var tableName = Encapsulate(type.Name);

            var tableattr = type.GetCustomAttributes(true).SingleOrDefault(attr => attr.GetType().Name == "TableAttribute") as dynamic;
            if (tableattr != null)
            {
                //tableName = String.Format("[{0}]", tableattr.Name);
                tableName = Encapsulate(tableattr.Name);
                try
                {
                    if (!String.IsNullOrEmpty(tableattr.Schema))
                    {
                        //tableName = String.Format("[{0}].[{1}]", tableattr.Schema, tableattr.Name);
                        string schemaName = Encapsulate(tableattr.Schema);
                        tableName = String.Format("{0}.{1}", schemaName, tableName);
                    }
                }
                catch (RuntimeBinderException)
                {
                    //Schema doesn't exist on this attribute.
                }
            }

            return tableName;
        }

        
        class TypeInfo
        {
            public string TableName;
            public List<PropInfo> All;
            public List<PropInfo> Keys;
            public List<PropInfo> Writebles;
            public List<PropInfo> Readables;
        }

        class PropInfo
        {
            public PropertyInfo Property;
            public string Name;
            public string ColumnName;
        }


    }
}
